Linguaggio C

 

per collaborazioni, commenti, critiche, e altro contattateci alla e-mail: clubinfo@libero.it risponderemo al più presto!

Funzioni e procedure

di Luca Sabatucci

Lezione 6

Pagina principale | Lezione precedente | Lezione successiva


Struttura di una funzione

Abbiamo già visto che la forma generale di una funzione è:


	specificatore_tipo nome_funzione ( elenco_parametri ) {
		istruzioni;
	}

Ogni funzione in C si assume che restituisca un valore. Da qui la necessità di specificare il tipo di funzione. Ad esempio:


int numero_di_accessi();

è una funzione che restituisce un intero, mentre


double radice_quadrata(...)

restituisce un numero in vigola mobile a doppia precisione. Una funzione può restituire un tipo di dato primitivo, una struttura (vedremo in seguito cosa è), un puntatore, ma non può restituire un vettore (anche questo lo vedremo in seguito).
Quando una funzione non deve restituire niente, si usa il tipo nullo: void. Ad esempio:


void cancella_lo_schermo();

L'elenco dei parametri indica quali variabili vengono "passate" alla funzione in modo che vi possa lavorare. Ad esempio data la funzione:


double radice_quadrata(double numero)

questa accetta in ingresso un double che viene posto in una variabile chiamata numero. L'elenco di più variabili viene effettuato mediante la virgola. Più variabili dello stesso tipo non possono essere accomunate; per ognuna di esse va specificato il tipo.


double distanza (int X1, int Y1, int X2, int Y2);

L'elenco dei parametri può essere vuoto, e in tal caso non si mette nulla tra le parentesi tonde, come nell'esempio della funzione cancella_lo_schermo.

A differenza del Pascal in C una funzione non può contenere altre funzioni. Non è possibile cioè definire una funzione dentro il corpo di un'altra.


double distanza (int X1, int Y1, int X2, int Y2) {
	double radice_quadrata(double numero) {
	...
	}
	
	...
}

Tutte le funzioni hanno la stessa visibilità e da una qualunque funzione si può avere accesso a tutte le altre. Questa è l'unica eccezione alla proprietà di linguaggio strutturato del C.
Tutte le funzioni devono essere dichiarate nel corpo principale del programma. E' buona norma elencare prima della definizione delle funzioni l'elenco dei prototipi.


#include<stdio.h>
#include<stdlib.h>

const double pi = 3.14;

double distanza (int X1, int Y1, int X2, int Y2);
double radice_quadrata(double numero);	 	

int main() {
	...
}

double distanza (int X1, int Y1, int X2, int Y2) {
	...
}

double radice_quadrata(double numero) {
	...
}

I prototipi di funzione consistono nella descrizione della funzione e sono uguali alla prima riga della dichiarazione della funzione fino (escluso) la parentesi graffa. Il loro impiego non è obbligatorio, ma risulta utile all'utente per individuare come si usa una certa funzione. Certe volte quando le funzioni sono molte, i prototipi e le costanti si mettono in un file a parte che in genere ha lo stesso nome del file che contiene il codice ma estensione .h, come ad esempio stdio.h; questo ne rende più semplice la consultazione.

Richiamare una funzione

Una volta che abbiamo definito una funzione, dobbiamo utilizzarla. Questo può essere fatto in qualunque punto in cui si possa inserire una istruzione, invocando il nome della funzione, seguito dalle parentesi tonde aperta e chiusa e dentro eventualmente i parametri di funzionamento. Ad esempio:


...
cancella_lo_schermo();
stampa("Eccomi qui!");
...

Nella prima funzione non sono richiesti parametri, mentre nella seconda si richiede una stringa da visualizzare su schermo.
Nel caso in cui la funzione restituisca qualche valore è possibile prelevarlo per effettuare delle operazioni.


valore = distanza(10,20,40,50);
valore += 20;

E' anche possibile utilizare il valore restituito da una funzione senza assegnarlo direttamente ad una variabile, direttamente in una espressione oppure come paramentro di un'altra funzione.


...
valore = calcola_area( distanza(x1,y1,x2,y2) + 20 );
...

Inserire più istruzioni in cascata in questa maniera a volte rende più leggibile un codice, altre volte invece ha l'effetto opposto. Dipende dal buon senso capire quando è il caso di spezzare una istruzione in più parti facendo uso di variabili temporanee.

Il passaggio dei parametri

Il passaggio dei parametri permette la comunicazione tra diverse entità (moduli) del programma. Il vantaggio nel passaggio dei parametri, rispetto all'uso di variabili globali, è che garantiscono maggiore controllo sui dati passati e rendono il codice più leggibile e maggiormente modificabile.

Il dato viene trasferito dal parametro attuale al parametro formale. Il parametro attuale è la variabile che contiene il dato che si vuole trasferire alla procedura. Ad esempio la stringa "Eccomi qui!" è il parametro attuale.
la variabile all'interno della funzione che è destinata ad accogliere tale valore viene chiamata parametro formale. Nell'esempio della funzione che calcola la distanza, X1, Y1, X2 e Y2 sono tutti parametri formali.

Il C come la maggior parte dei linguaggi usa il metodo posizionale per legare i parametri attuali a quelli formali. Se una funzione ha il seguente prototipo:


void inserisci(int peso, double altezza, char iniziale_nome, char iniziale_cognome);

il cui scopo è quello di aggiornare un database medico, e si effettua la seguente chiamata:


inserisci(80,1.75,'L','S');

In effetti è la posizione che indica la reciproca corrispondenza:

peso = 80
altezza=1.75
iniziale_nome = 'L'
iniziale_cognome = 'S'

Esistono diverse metodologie per il passaggio dei parametri.

Nel C (a differenza di altri linguaggi come il Pascal) solo il passaggio per copia è supportato. Tuttavia è possibile simulare il passaggio per indirizzo capendo esattamente di che si tratta. Il passaggio per indirizzo non è altro (come dice il nome) il fornire alla procedura l'indirizzo di memoria in cui c'è il contenuto della variabile specificata. La funzione quindi non alloca spazio per una nuova variabile, ma crea una variabile alias. Ovvero assegna temporaneamente alla variabile (parametro attuale) un nuovo nome (parametro formale) con cui lavora. Ma le modifiche sono comuni alle due variabili, perchè in effetti si tratta della stessa variabile.

Quindi, poichè in C è possibile lavorare direttamente con gli indirizzi di memoria (puntatori), è possibile adoperare questa struttura abbastanza semplicemente. Vedremo nella lezione sui puntatori come passare una variabile per indirizzo.

Restituire un valore: l'istrzione return

Il passaggio per indirizzo tuttavia non è l'unico modo per scambiare dati tra la funzione e il contesto in cui è stata invocata. Il più semplice lo abbiamo già visto più di una volta è fare restituire un valore alla funzione. Abbiamo visto come di dichiara una funzione che restituisce un valore. Vediamo adesso come fa la funzione a restituire il valore voluto.

Creiamo una funzione che calcola il massimo tra due numeri interi:


int massimo(int a,int b) {
	int max;

	if (a > b)
		max = a;
	else
		max = b;

	return max;
}

Allora... si dichiara una variabile che conterrà il valore più grande tra a e b. Una volta assegnato il valore a max, questo viene reso disponibile all'esterno mediante l'istruzione return.

Definiamo adesso una funzione che utilizza la funzione massimo per determinare il massimo tra 3 numeri:


int massimo_3(int a,int b,int c) {
	int max;

	max = massimo(a,b);
	max = massimo(max,c);

	return max;
}

Facciamo alcune considerazioni. La nuova funzione massimo_3 richiama la funzione massimo. Il valore restituito dalla funzione può essere usato in una espressione o assegnato ad una variabile (lo abbiamo già visto). La funzione massimo e massimo_3 hanno la stessa variabile locale max. Ma non si tratta della stessa variabile, bensì di due variabili con lo stesso nome. Da nessuna funzione (compresa la main) è possibile accedere alle variabili interne ad un'altra.

Una variante della funzione massimo_3 è la seguente:


int massimo_3(int a,int b,int c) {
	int max;

	max = massimo(massimo(a,b),c);

	return max;
}

In cui le operazioni compiute sono le stesse che nel caso precedente, ma non si effettua l'assegnamento temporaneo.

L'istruzione return serve anche a uscire da una funziome:


int massimo(int a,int b) {
	if (a > b)
		return a;
	else
		return b;
}

oppure


void scrivi_su_file(char carattere) {

	if (carattere == ' ')
		return;

	...

}

Nel secodo esempio appare ancora più evidente l'uso dell'istruzione per terminare la funzione senza la necessità di eseguire tutte le istruzioni al suo interno. Un uso intenso di istruzioni return dislocate in punti interni ad una funzione diventano uno scoglio per chi deve capire il comportaento della funzione (soprattutto se la funzione è lunga molte righe di codice). Meglio sempre mettere l'istruzione return all'inizio o alla fine della funzione.

Ricorsione

Una grossa potenzialità del C è che una funzione può richiamare se stessa. Una funzione al cui interno è presente una chiamata a se stessa è detta ricorsiva.
Un tipico esempio di funzione ricorsiva è quella usata per calcolare il fattoriale di un intero positivo:


int fattoriale(int n) {
	if (n == 1)
		return 1;
	else
		return n*fattoriale(n-1);
}

Il fattoriale di un numero n è il prodotto dei primi n numeri interi, ad esempio il fattoriale di 4 (che si indica con 4!) è: 1 * 2 * 3 * 4 = 24

Nella funzione fattoriale abbiamo due casi: il fattoriale di 1 è 1. Per gli altri numeri si sfrutta la proprietà che !n = n * (n-1)!
La condizione n == 1 si chiama condizione di uscita, e garantisce che il processo di ricorsione abbia una fine.

Lo stesso risultato sia ottenibile mediante un ciclo for:


int fattoriale(int n) {
	int parziale = 1;
	int cont;
	
	for (cont = 1;cont &l;= n; cont++)
		parziale *=cont;

	return parziale;
}

si vede subito come la prima delle due, quella che usa la ricorsione, è molto più esplicativa rispetto all'altra. Tutti i risultati ottenuti mediante funzioni ricorsive possono essere ottenuti mediante funzioni non ricorsive, che in genere sono molto più complesse. Il vantaggio di usare la ricorsione è che determinati problemi trovano in essa una soluzione elegante, chiara e semplice. Vedremo ad esempio per le liste, gli alberi e le strutture dinamiche in genere come questa affermazione sia veritiera. Tuttavia la ricorsività non è una manna. Una funzione ricorsiva è un'arma a doppio taglio. Se infatti una funzione ricorsiva è costruita male mostrerà subito i suoi limiti. In genere il problema connesso con la ricorsività è la facilità con cui il processo termina la memoria del computer, per quanto grande essa sia.

Per chiarire questo concetto vediamo come viene gestita una chiamata di funzione ricorsiva. Introduciamo brevemente il conetto di stack o pila. Uno stack è una zona di memoria del computer in cui vengono memorizzati dei dati, uno sopra gli altri, proprio come se fosse una pila di libri. Finchè c'è spazio si può aggiungere un libro, ma se il libro viene ricoperto, bisogna aspettare di prendere tutti i libri che stanno sopra prima di prendere quello.

Supponiamo che il libro giallo sia il dato che ci interessa. Lo poniamo nello stack. Questo viene ricoperto da altri dati; prima di prelevarlo dobbiamo essere sicuri che i libri aggiunti siano stati tolti.

Ma a che serve? Quando in C viene richiamata una funzione si fa ricorso allo stack. Supponiamo che il processore stia eseguendo le istruzioni di una funzione. Si dice che il sistema si trova in un determnato contesto, con certe variabili che assumono certi valori. Adesso però il processore incontra una chiamata di funzione. Tutti i dati e le variabili attuali temporaneamente non servono più perchè come sappiamo ogni funzione ha il suo ambiente locale. Tuttavia non devono essere dimenticate perchè quando la funzione terminerà dovranno essere ripristinati. Quindi immaginiamo di impacchettare queste variabili e di metterle nello stack (nel quadretto giallo, figura 2).
Nella funzione invocata ci sono nuove variabili e nuovi valori per queste. Supponiamo che anche qui ci sia una chiamata ad una terza funzione. Allora per una seconda volta tutte le variabili locali vengono impacchettate e messe nello stack (figura 3). Quando la terza funzione termina, dallo stack vengono prelevate le variabili "congelate" che vengono ripristinate come se nulla fosse successo (figura 4). Lo stesso avviene quando anche la seconda funzione termina: si ripristina dallo stack le variabili relative all'ambiente iniziale (figura 5).

Questa introduzione ci è servita per spiegare che ogni chiamata di funzione genera una certa occupazione di memoria nello stack. Quando la funzione termina questa memoria viene liberata.

Analizziamo questa funzione ricorsiva


int ricorsione() {
	return ricorsione();
}

essa richiama se stessa, e poi se stessa e così via all'infinito. Non esiste alcuna possibilità che essa termini. Dal punto di vista pratico invece essa termina presto, ma con un bel segnale di errore di esaurimento della memoria.
Infatti ad ogni successiva invocazione viene allocato dello spazio nello stack, il quale non viene mai rilasciato. Anche il computer più dotato di memoria terminerà in pochi secondi tutta la memoria a sua disposizione, impedendo il proseguio delle operazioni.

Approfondiremo questo concetto in seguito con degli esempi pratici.

Variabili static

Le variabili static sono variabili permanenti con una visisbilità locale ristretta ad una funzione.
Esempio:


void crea_finestra() {
	static int numero_volte=0;

	numero_volte++;
	...
}

In questa funzione, mediante la quale si richiede di creare una nuova finestra, viene dichiarata una variabile static numero_volte. A differenza delle comuni variabili locali, la variabile viene creata insieme a tutte le variabili globali. L'inizializzazione viene effettuata soltanto una volta, e la variabile continua ad esiste anche quando la funzione termina. Tuttavia a differenza di una variabile globale solo dentro la funzione in cui è dichiarata si ha accesso al suo contenuto.

Sfruttando il fatto che tra una chiamata e la successiva, questa variabile mantiene il suo valore nel nostro esempio la usiamo per ricordare il numero di volte che la funzione crea_finestra è richiamata. Esistono situazioni in cui questo può essere utile, ad esempio se dobbiamo imporre un nome casuale alla finestra possiamo usare questo numero.

Questo genere di variabili vengono usate molto raramente e in casi particolarissimi per evitare l'uso di una variabile globale.


Bibliografia


Testi consigliati per l'apprendimento

Questo articolo è stato scaricato dal Club di informatica
Pagina curata da Luca Sabatucci